#!/usr/bin/env python
#
# Copyright 2009 Facebook
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

import concurrent.futures
import os.path
import re
import logging
#import subprocess
import datetime
import uuid

import peewee
#import psycopg2
from playhouse.pool import PooledPostgresqlExtDatabase
import markdown
import bcrypt
import unicodedata

#import torndb
import tornado.escape
from tornado import gen
import tornado.httpserver
import tornado.ioloop
import tornado.options
import tornado.websocket
import tornado.web
from tornado.concurrent import Future
import tornadoredis

#from .models import Author,Entry

from tornado.options import define, options

logging.basicConfig(level=logging.DEBUG)

define("port", default=8888, help="run on the given port", type=int)
# define("mysql_host", default="127.0.0.1:3306", help="blog database host")
# define("mysql_database", default="blog", help="blog database name")
# define("mysql_user", default="blog", help="blog database user")
# define("mysql_password", default="blog", help="blog database password")

define("pg_host", default="127.0.0.1", help="blog database host")
define("pg_database", default="hive", help="blog database name")
define("pg_user", default="hive", help="blog database user")
define("pg_password", default="hive", help="blog database password")

# A thread pool to be used for password hashing with bcrypt.
executor = concurrent.futures.ThreadPoolExecutor(2)
CONNECTION_POOL = tornadoredis.ConnectionPool(max_connections=10,
                                              wait_for_available=True)

NUMBER_OF_CLIENTS = 5



class Application(tornado.web.Application):
    def __init__(self):
        handlers = [
            (r"/", HomeHandler),
            (r"/archive", ArchiveHandler),
            (r"/feed", FeedHandler),
            (r"/entry/([^/]+)", EntryHandler),
            (r"/compose", ComposeHandler),
            (r"/auth/create", AuthCreateHandler),
            (r"/auth/login", AuthLoginHandler),
            (r"/auth/logout", AuthLogoutHandler),
            (r"/chat", ChatHandler),
            (r"/a/message/new", MessageNewHandler),
            (r"/a/message/updates", MessageUpdatesHandler),
            (r"/chat-socket-home", ChatSocketHomeHandler),
            (r"/chatsocket", ChatSocketHandler),
            (r"/redis", RedisHandler),
        ]
        settings = dict(
            blog_title=u"Tornado Blog",
            template_path=os.path.join(os.path.dirname(__file__), "templates"),
            static_path=os.path.join(os.path.dirname(__file__), "static"),
            ui_modules={
                "Entry": EntryModule
            },
            xsrf_cookies=True,
            cookie_secret="__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__",
            login_url="/auth/login",
            debug=True, )
        super(Application, self).__init__(handlers, **settings)


class BaseHandler(tornado.web.RequestHandler):
    @property
    def db(self):
        return self.application.db
    @property
    def redis(self):
        c = tornadoredis.Client(connection_pool=CONNECTION_POOL)
        return c
    
    def get_current_user(self):
        user_id = self.get_secure_cookie("blogdemo_user")
        if not user_id:
            return None
        user = None
        try:
            user = Author.get(Author.id == user_id)
        except Author.DoesNotExist:
            user = None
        return user
        #return self.db.get("SELECT * FROM authors WHERE id = %s", int(user_id))

    def any_author_exists(self):
        return Author.select().count() > 0
        #return bool(self.db.get("SELECT * FROM authors LIMIT 1"))


class HomeHandler(BaseHandler):
    def get(self):
        #entries = self.db.query("SELECT * FROM entries ORDER BY published "
        #                        "DESC LIMIT 5")
        entries = Entry.select().order_by(-Entry.id).paginate(1, 10)
        if not entries:
            self.redirect("/compose")
            return
        logging.debug('hello')
        self.render("home.html", entries=entries)


class EntryHandler(BaseHandler):
    def get(self, slug):
        entry = Entry.get(Entry.slug == slug)
        #entry = self.db.get("SELECT * FROM entries WHERE slug = %s", slug)
        if not entry:
            raise tornado.web.HTTPError(404)
        self.render("entry.html", entry=entry)


class ArchiveHandler(BaseHandler):
    def get(self):
        entries = Entry.select().order_by(-Entry.id)
        #entries = self.db.query("SELECT * FROM entries ORDER BY published "
        #                        "DESC")
        self.render("archive.html", entries=entries)


class FeedHandler(BaseHandler):
    def get(self):
        #entries = self.db.query("SELECT * FROM entries ORDER BY published "
        #                        "DESC LIMIT 10")
        entries = Entry.select().order_by(-Entry.id).paginate(1, 10)
        self.set_header("Content-Type", "application/atom+xml")
        self.render("feed.xml", entries=entries)


class ComposeHandler(BaseHandler):
    @tornado.web.authenticated
    def get(self):
        id = self.get_argument("id", None)
        entry = None
        if id:
            #entry = self.db.get("SELECT * FROM entries WHERE id = %s", int(id))
            entry = Entry.get(Entry.id == int(id))
        self.render("compose.html", entry=entry)

    @tornado.web.authenticated
    def post(self):
        id = self.get_argument("id", None)
        title = self.get_argument("title")
        text = self.get_argument("markdown")
        html = markdown.markdown(text)
        if id:
            #entry = self.db.get("SELECT * FROM entries WHERE id = %s", int(id))
            try:
                entry = Entry.get(Entry.id == int(id))
            except Entry.DoesNotExist:
                raise tornado.web.HTTPError(404)
            slug = entry.slug
            entry.title = title
            entry.input_content = text
            entry.display_content = html
            entry.save()
            #self.db.execute(
            #    "UPDATE entries SET title = %s, markdown = %s, html = %s "
            #    "WHERE id = %s", title, text, html, int(id))
        else:
            slug = unicodedata.normalize("NFKD", title).encode("ascii",
                                                               "ignore")
            slug = slug.decode('utf-8')
            slug = re.sub(r"[^\w]+", " ", slug)
            slug = "-".join(slug.lower().strip().split())
            if not slug:
                slug = "entry"
            while True:
                try:
                    #e = self.db.get("SELECT * FROM entries WHERE slug = %s", slug)
                    e = Entry.get(Entry.slug == slug)
                except Entry.DoesNotExist:
                    break
                #if not e: 
                #    break
                slug += "-2"
            #self.db.execute(
            #    "INSERT INTO entries (author_id,title,slug,markdown,html,"
            #    "published) VALUES (%s,%s,%s,%s,%s,UTC_TIMESTAMP())",
            #    self.current_user.id, title, slug, text, html)
            e = Entry(
                author=self.get_current_user(),
                title=title,
                slug=slug,
                input_content=text,
                display_content=html,
                input_format='markdown')
            e.save()
        self.redirect("/entry/" + slug)


class AuthCreateHandler(BaseHandler):
    def get(self):
        self.render("create_author.html")

    @gen.coroutine
    def post(self):
        if self.any_author_exists():
            raise tornado.web.HTTPError(400, "author already created")
        hashed_password = yield executor.submit(
            bcrypt.hashpw, tornado.escape.utf8(self.get_argument("password")),
            bcrypt.gensalt())
        #author_id = self.db.execute(
        #    "INSERT INTO authors (email, name, hashed_password) "
        #    "VALUES (%s, %s, %s)",
        #    self.get_argument("email"), self.get_argument("name"),
        #    hashed_password)
        u = Author(
            email=self.get_argument('email'),
            name=self.get_argument('name'),
            password=hashed_password)
        u.save()
        author_id = u.id
        self.set_secure_cookie("blogdemo_user", str(author_id))
        self.redirect(self.get_argument("next", "/"))


class AuthLoginHandler(BaseHandler):
    def get(self):
        # If there are no authors, redirect to the account creation page.
        if not self.any_author_exists():
            self.redirect("/auth/create")
        else:
            self.render("login.html", error=None)

    @gen.coroutine
    def post(self):
        #author = self.db.get("SELECT * FROM authors WHERE email = %s",
        #                     self.get_argument("email"))
        author = None
        try:
            author = Author.get(Author.email == self.get_argument('email'))
        except Author.DoesNotExist:
            author = None
        if not author:
            self.render("login.html", error="email not found")
            return
        hashed_password = yield executor.submit(
            bcrypt.hashpw, tornado.escape.utf8(self.get_argument("password")),
            tornado.escape.utf8(author.password))
        if hashed_password == author.password:
            self.set_secure_cookie("blogdemo_user", str(author.id))
            self.redirect(self.get_argument("next", "/"))
        else:
            self.render("login.html", error="incorrect password")


class AuthLogoutHandler(BaseHandler):
    def get(self):
        self.clear_cookie("blogdemo_user")
        self.redirect(self.get_argument("next", "/"))


class EntryModule(tornado.web.UIModule):
    def render(self, entry):
        return self.render_string("modules/entry.html", entry=entry)

class MessageBuffer(object):
    def __init__(self):
        self.waiters = set()
        self.cache = []
        self.cache_size = 200

    def wait_for_messages(self, cursor=None):
        # Construct a Future to return to our caller.  This allows
        # wait_for_messages to be yielded from a coroutine even though
        # it is not a coroutine itself.  We will set the result of the
        # Future when results are available.
        result_future = Future()
        if cursor:
            new_count = 0
            for msg in reversed(self.cache):
                if msg["id"] == cursor:
                    break
                new_count += 1
            if new_count:
                result_future.set_result(self.cache[-new_count:])
                return result_future
        self.waiters.add(result_future)
        return result_future

    def cancel_wait(self, future):
        self.waiters.remove(future)
        # Set an empty result to unblock any coroutines waiting.
        future.set_result([])

    def new_messages(self, messages):
        logging.info("Sending new message to %r listeners", len(self.waiters))
        for future in self.waiters:
            future.set_result(messages)
        self.waiters = set()
        self.cache.extend(messages)
        if len(self.cache) > self.cache_size:
            self.cache = self.cache[-self.cache_size:]


# Making this a non-singleton is left as an exercise for the reader.
global_message_buffer = MessageBuffer()


class ChatHandler(tornado.web.RequestHandler):
    def get(self):
        self.render("chat.html", messages=global_message_buffer.cache)


class MessageNewHandler(tornado.web.RequestHandler):
    def post(self):
        message = {
            "id": str(uuid.uuid4()),
            "body": self.get_argument("body"),
        }
        # to_basestring is necessary for Python 3's json encoder,
        # which doesn't accept byte strings.
        message["html"] = tornado.escape.to_basestring(
            self.render_string("message.html", message=message))
        if self.get_argument("next", None):
            self.redirect(self.get_argument("next"))
        else:
            self.write(message)
        global_message_buffer.new_messages([message])


class MessageUpdatesHandler(tornado.web.RequestHandler):
    @gen.coroutine
    def post(self):
        cursor = self.get_argument("cursor", None)
        # Save the future returned by wait_for_messages so we can cancel
        # it in wait_for_messages
        self.future = global_message_buffer.wait_for_messages(cursor=cursor)
        messages = yield self.future
        if self.request.connection.stream.closed():
            return
        self.write(dict(messages=messages))

    def on_connection_close(self):
        global_message_buffer.cancel_wait(self.future)




db = PooledPostgresqlExtDatabase(
    options.pg_database,
    max_connections=10,
    stale_timeout=300,
    user=options.pg_user,
    password=options.pg_password,
    host=options.pg_host)


class BaseModel(peewee.Model):
    class Meta:
        database = db


class Author(BaseModel):
    email = peewee.CharField(max_length=100)
    name = peewee.CharField(max_length=100)
    password = peewee.CharField(max_length=100)

    class Meta:
        db_table = 'blog_author'


class Entry(BaseModel):
    author = peewee.ForeignKeyField(Author)
    slug = peewee.CharField(max_length=1024)
    title = peewee.CharField(max_length=1024)
    input_content = peewee.TextField()
    input_format = peewee.CharField(max_length=255, default='markdown')
    display_content = peewee.TextField()
    created_at = peewee.DateTimeField(default=datetime.datetime.now)
    updated_at = peewee.DateTimeField(default=datetime.datetime.now)

    class Meta:
        db_table = 'blog_entry'


class ChatSocketHomeHandler(tornado.web.RequestHandler):
    def get(self):
        self.render("chat-socket/index.html", messages=ChatSocketHandler.cache)

class ChatSocketHandler(tornado.websocket.WebSocketHandler):
    waiters = set()
    cache = []
    cache_size = 200

    def get_compression_options(self):
        # Non-None enables compression with default options.
        return {}

    def open(self):
        ChatSocketHandler.waiters.add(self)

    def on_close(self):
        ChatSocketHandler.waiters.remove(self)

    @classmethod
    def update_cache(cls, chat):
        cls.cache.append(chat)
        if len(cls.cache) > cls.cache_size:
            cls.cache = cls.cache[-cls.cache_size:]

    @classmethod
    def send_updates(cls, chat):
        logging.info("sending message to %d waiters", len(cls.waiters))
        for waiter in cls.waiters:
            try:
                waiter.write_message(chat)
            except:
                logging.error("Error sending message", exc_info=True)

    def on_message(self, message):
        logging.info("got message %r", message)
        parsed = tornado.escape.json_decode(message)
        chat = {
            "id": str(uuid.uuid4()),
            "body": parsed["body"],
            }
        chat["html"] = tornado.escape.to_basestring(
            self.render_string("chat-socket/message.html", message=chat))

        ChatSocketHandler.update_cache(chat)
        ChatSocketHandler.send_updates(chat)


class RedisHandler(tornado.web.RequestHandler):
    
    @tornado.gen.engine
    def incr_counter(self, client, multiplier, callback=None):
        k = 'counter%d' % multiplier
        res = yield tornado.gen.Task(client.incrby, k, multiplier)
        # Ensure that demo keys will expire in 2 minutes.
        # Note that to execute this command a client has to wait
        # for other clients to complete their INCRBY calls.
        # You may check it using the redis-cli command-line utility
        # and the MONITOR command.
        yield tornado.gen.Task(client.expire, k, 120)
        # Return the number of visits multiplied by specified value
        callback(res)

    @tornado.web.asynchronous
    @tornado.gen.engine
    def get(self):
        '''
        Register the number of page views and return it on the page.
        Create NUMBER_OF_CLIENTS redis clients and connect them
        to the global connection pool.
        '''
        indexes = range(1, NUMBER_OF_CLIENTS + 1)
        clients = (tornadoredis.Client(connection_pool=CONNECTION_POOL)
                   for __ in indexes)
        # Register page views using %NUMBER_OF_CLIENTS% redis clients.
        # Run redis commands "simultaneously" to simulate a complex
        # application creating a number of Client instances.
        values = yield map(lambda c, n: tornado.gen.Task(self.incr_counter,
                                                         c, n),
                           clients,
                           indexes)

        # Create a new client and get the redis server information
        # to display it on a demo page.
        c = tornadoredis.Client(connection_pool=CONNECTION_POOL)
        info = yield tornado.gen.Task(c.info)
        values = map(lambda n, v: (n, v), indexes, values)
        self.render("redis.html",
                    title="Connection Pool Demo",
                    values=values,
                    info=info)


def main():
    tornado.options.parse_command_line()
    http_server = tornado.httpserver.HTTPServer(Application())
    http_server.listen(options.port)
    tornado.ioloop.IOLoop.current().start()


if __name__ == "__main__":
    main()
